JEP 400 和默认字符集

TL;DR: 从 JDK 18 开始,UTF-8 是跨平台的默认字符集。请确保测试您的应用程序,尤其是在 Windows 上运行时。


Close-up of acient characters
照片由 Raphael Schaller 提供

您是否曾经想过“默认字符集”?以下是 Charset.defaultCharset javadoc 中的说明

默认字符集在虚拟机启动期间确定,通常取决于底层操作系统的区域设置和字符集。

短语“取决于底层操作系统的区域设置和字符集”听起来有点太模糊了。为什么呢?当 Java 在 25 年前推出时,还没有默认字符集这样的东西。当时,Java 语言规范采用 Unicode 作为 java.lang.Character 类的基础,这是一个明智的选择。快进到今天,Unicode 现在更加普遍。如今,UTF-8 编码几乎在所有地方都占主导地位,尤其是在网络上,超过 95% 的内容使用 UTF-8 编码(参见 按排名细分的字符编码使用情况)。

UTF-8 维基百科页面 证实了这些年来 UTF-8 的增长。


较新的编程语言(例如 Go、Rust)采用 UTF-8 作为默认文本编码。在 Java 中,方法 Charset.defaultCharset() 返回取决于底层操作系统/用户环境的任意字符集,这通常被认为是用户肩上的技术债务。新开发人员不应该处理这种历史债务。

从另一个角度来看,即“默认字符集在哪里使用?”最典型的使用可能是 java.io.InputStreamReader 类的隐式解码器。看一下 java.io.FileReader,它是 InputStreamReader 的子类。假设一个用 UTF-8 编码的日语文本文件被一个 FileReader 实例读取,该实例是在没有指定显式字符集的情况下创建的

java.io.FileReader("test.txt") "こんにちは" (macOS) java.io.FileReader("test.txt") "ã?“ã‚“ã?«ã?¡ã? ̄" (Windows (en-US))

这里,问题很明显。在 macOS 上,底层操作系统使用的默认编码是 UTF-8,因此文件内容被正确读取(解码)。另一方面,如果您在 Windows(美国)上读取同一个文本文件,内容将是乱码。这是因为 FileReader 对象使用代码页 1252 编码读取文本内容,这是 Windows 在系统区域设置 英语(美国) 中使用的默认编码。即使在同一个操作系统上,结果也可能因用户的设置而异。如果该 Windows 主机的用户将系统区域设置更改为 日语(日本),那么他/她将得到

java.io.FileReader("test.txt") "縺薙s縺ォ縺。縺ッ" (Windows (ja-JP))

总得有人做出改变!


将 UTF-8 设为默认字符集

为了解决这个长期存在的问题,JEP 400 正在将 JDK 18 中的默认字符集更改为 UTF-8。这实际上与 java.nio.file.Files 类的现有 newBufferedReader/Writer 方法一致,在没有设置显式字符集的情况下,UTF-8 是默认字符集。

jshell> Files.newBufferedReader(Path.of("test.txt")).readLine()
$1 ==> "こんにちは"

上面的示例表明,从 JDK 17 开始,可以使用 java.nio.file.Files 方法读取 UTF-8 编码的文本文件,而无需考虑主机和/或用户的设置。

通过将 UTF-8 设为默认字符集,JDK I/O API 现在将始终以相同且可预测的方式工作,无需关注主机和/或用户的环境!以前需要一致行为的应用程序需要指定不受支持的 file.encoding 系统属性。这不再需要了!

jshell> new BufferedReader(new FileReader("test.txt")).readLine()
$2 ==> "こんにちは"

上面的示例表明,FileReader 类现在可以与更新的 Files 方法一致地工作,而无需考虑 JDK 18 中主机和/或用户的设置。

需要解决一个问题。那就是,System.out/err 直接连接到底层的 stdout/err,它遵循底层主机和/或用户的环境。如果我们将该编码更改为 UTF-8,那么对 System.out/err 的任何输出都会立即受到影响,并在某些环境(例如 Windows)中出现乱码。出于这个原因,这些 I/O 中使用的编码保持不变,这等效于 JDK 17 中引入的 java.io.Console.charset()


兼容性和缓解策略

将默认字符集更改为 UTF-8 是正确的做法(而且早就应该这样做了),但它确实会带来一些不兼容问题,尤其是对于仅部署在 Windows 上的应用程序而言。我们理解一些用户确实期望之前的行为,即默认字符集取决于主机和用户的环境。为了使这些应用程序能够一致地工作,我们提供了以下两种缓解措施

1. 源代码重新编译

如果您有能力重新编译源代码,那么将受影响的代码更改为显式指定字符集。例如,在上面的示例中,将那些没有字符集的构造函数替换为具有显式字符集的构造函数,例如 java.io.FileReader("test.txt", "UTF-8")。通过这样做,行为将保持一致。如果您不知道字符集,但仍然想要之前的行为,请使用 JDK 17 中引入的 native.encoding 系统属性。例如,在 Windows 的 英语(美国) 系统默认区域设置中

jshell> System.getProperty("native.encoding")
$3 ==> "Cp1252"

因此,您需要将 Cp1252 指定给 FileReader 构造函数。修改将如下所示

String encoding = System.getProperty("native.encoding"); // Populated on Java 18 and later
Charset cs = (encoding != null) ? Charset.forName(encoding) : Charset.defaultCharset();
var reader = new FileReader("file.txt", cs);

说到编译,javac 命令也依赖于默认字符集。因此,您需要知道源文件保存的编码方式,它可能是也可能不是 UTF-8,并使用 javac-encoding 选项指定它。

2. 不重新编译

在 JDK 18 中,file.encoding 已成为一个受支持的系统属性(即,在 javadoc 中描述并受支持)。该系统属性的值可以是 UTF-8COMPAT,否则行为未定义。如果应用程序使用 -Dfile.encoding=COMPAT 命令行选项启动,那么默认编码将按照之前 JDK 版本中的方式确定,从而保留兼容性。


为 JEP 400 做准备 - 行动号召

由于 JEP 400 是一种具有破坏性的增强功能,我们敦促您使用现有环境测试您的应用程序。此 JEP 的确切影响可以通过使用 file.encoding 系统属性,在 JDK 8 及更高版本的先前发布的 JDK 中轻松重现。因此,尝试使用 -Dfile.encoding=UTF-8 命令行选项运行您的应用程序,并查看它的行为。我们预计在 macOS 和 Linux 上不会出现任何问题,因为它们的默认编码已经是 UTF-8。在 Windows 上,尤其是对于中文/日文/韩文等东亚语言区域设置,可能会出现一些不兼容的行为。如果是这种情况,请尝试上面解释的缓解策略。

当然,您也可以使用 JDK 18 早期访问版本(JEP 400 已集成到版本 13 中)试用 JEP 400,该版本可以从 https://jdk.java.net/18/ 下载。


总结

我们想知道 JEP 400 的接受程度,因为它是一个早就应该出现但具有破坏性的增强功能。当 JEP 被提升到“候选”状态时,我们收到了很多外部反馈,结果表明大多数反馈都是非常积极的!这加强了对这种增强功能所采取的方向的信心。我们相信,从长远来看,它将被开发人员遗忘,因为它变得如此商品化。